<?php
// +----------------------------------------------------------------------
// | 杭州十禾信息科技有限公司
// +----------------------------------------------------------------------
// | Copyright (c) 2013-2015 http://www.shelpe.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: LeePuman <ysyzlsm@163.com>
// +----------------------------------------------------------------------

interface TaobaoWordInterface{

	//获取全网多个词语的在一段时间内的统计属性
	public function getAvgProps($words, $fields, $startDate=null, $endDate=null);
	
	//获取全网多个词语在一段时间内的序列统计属性（即按时序排列的一组数组）
	public function getSerialProps($word, $fields, $startDate, $endDate);
	
	//获取一个或者多个词语某个分析最新属性 @TODO 该结构稍后应该把getAnalysis废弃掉，没有地方只用，而且比较麻烦
	/**
	 * @deprecated
	 */
	public function getWordAnalysis($words,  $fields='hprice', $useApi = true);
	
	//从本地获取一个或者多个关键词一段时间内的多个分析属性，本方法只是用来在本地获取，上面一个方法已经有了
	//public function getAnalysis($words, $fields, $startDate, $endDate);
	
	//通过淘宝API获取某些词的相关词
	public function getRelatedWords($words, $useApi = false, $mergeRes = false , $useSearch = false );
	
	//通过淘宝搜索URL获取某些词的相关词
	public function getSearchRelated($words , $useSearchRealtime = false);
	
	//获取类目属性的平均统计值
	public function getWordCatsBasicAvgProps($words, $cateId, $from = null, $to  = null , $useApi = true);
	
	//获取类目热词
	public function getCatHotWords($cate_ids , $num = 100 ,$merge = true , $useApi = true );
	
	//获取类目基础统计属性
	public function getCategoryAvgBase( $cates_id , $fields , $startDate = null, $endDate = null);
		
}

class WordApiModel {

	/** @var int Max number of related words from API */
	const MAX_API_RELATED_WORDS = 10;
	/** @var int Max number of related words from Search Suggestion */
	const MAX_SEARCH_RELATED_WORDS = 10;
	/** @var 批量查询类目词分析属性的chunk的大小 */
	const MAX_WORDS_ANALYSIS_PROPS_PER_REQUEST  = 50;
	/** @var 批量查询词全网统计属性的chunk大小 */
	const MAX_WORDS_BASIC_PROPS_PER_REQUEST = 100;
	
	
    private $_autoSave = true;
    /** ApiModel singlone model */
    private $_apiMdl = null;
    
    
    protected $_junky_words = array();
    
    /**
     * 是否打开auto save，打开的时候自动保存查询结果到数据库中，否则只是返回结果
     * @param bool $open, true就是打开，false就是关闭
     * @return void
     */
    public function setAutoSave( $open = true ){
        $this->_autoSave = $open ;
    }
    
    /**
     * 检查在获取之后是否需要保存数据
     */
    protected function checkSaveData( $entity , $dataList , $options = array() ){
        
        if( !$this->_autoSave ) return ;
        switch( $entity ){
            case 'word_basic_props': 
                $handler = $this->_getWordBasicPropMdl();
            break;
            
            case 'word_analysis_props':
                $handler = $this->_getWordAnalysisPropMdl();
            break;
            
            case 'word_related':
                $handler = $this->_getWordRelatedMdl();
            break;
            
            case 'word_cats_props':
            	$handler = $this->_getWordCatsMdl();
            break;
            
            
            case 'word_hot':
                $handler = $this->_getWordHotdMdl();
            break;
        }
        
		if( $handler && method_exists( $handler , 'saveDataList')){
			try{
		    	$handler->saveDataList($dataList , $options);
			}catch( Exception $e ){
				Log::record("通过api获取数据时保存失败，错误如下:"  . $e->getMessage() , Log::ERR);
				Log::record("TRACE:"  . $e->getTraceAsString() , Log::ERR);
			} 
		}else{
		    throw new NewzanException('没有找到存储使用的handler' , RetStatus::INNER_ERR);
		}
    }

    
    /**
     * @deprecated 
     * @param unknown $words
     * @param unknown $fields
     * @param unknown $startDate
     * @param unknown $endDate
     * @return multitype:multitype:
     */
	protected function getAvgProps($words, $fields, $startDate, $endDate){
		//跳过参数验证，所有参数验证必须在对外公开的类中进行，所有api方法不对外公开
		//分成小块查询，当需要查询的词语比较多的时候
		$need_api_words = array_chunk($words, self::MAX_WORDS_BASIC_PROPS_PER_REQUEST);
		//调用子类方法
		$apiMdl = $this->_getApiMdl();
		$words_props_from_api = array();
		$startTime = strtotime($startDate);
		$endTime = strtotime($endDate);
		$nowTime = strtotime( date('Y-m-d') );
		//get the time span for api
        $weekdiff = floor( ($nowTime - $startTime) / 86400 / 7 );
        $timespan = 'WEEK';
        if( $weekdiff > 4 ) $timespan = 'MONTH';
        if( $weekdiff > 8 ) $timespan = '3MONTH';
        
		for( $i=0; $i < count($need_api_words); $i++ ){
			//一次性查3个月的表现,当出现api错误的时候跳过此chunk，本组词全部为NULL
			$results = $apiMdl->getWordProps( $need_api_words[$i] , $timespan);
			//当结果为NULL的时候就是API发生了一些问题，而不是关键词没数据
			if( $results === null ) continue;
			//save to db
			$dataList = array();
			$key_fields = array_flip( $fields );
			//有数据的词语
			$words_with_data = array();
			foreach( $results as $word_in_records){
				//$word_id =  $word_ids[ $word_in_records->word ];
				$word =  (string) $word_in_records->word;
				$words_with_data[] = $word;
				$words_props_from_api[ $word ] = array();
				//计算avg_price的时候使用加权平均
				$total_competition = 0;
				$avg_price_nu = 0;
				foreach( $word_in_records->in_record_base_list->i_n_record_base as $row ){
					$row = object_to_array( $row );
					$row['word'] = $word;
					$dataList[] = $row;
					if( strtotime($row['date']) <= $endTime && strtotime($row['date']) >= $startTime ){
					    //array_merge_recursive此处可以用，因为key都是字符串
						$words_props_from_api[$word] = array_merge_recursive($row , $words_props_from_api[$word]);
						$total_competition += intval($row['competition']);
						$avg_price_nu += intval($row['avg_price']) * intval($row['competition']);
					}
				}
// 				echo '<pre>';
// 				print_r($words_props_from_api[$word]);
				//日期属性去掉，因为是求平均值
				unset($words_props_from_api[$word]['date']);
				foreach( $words_props_from_api[$word]  as  $wk => $values){
					$pricision = $wk == 'ctr' ? 3 : 0;
					$words_props_from_api[$word][$wk] = round( array_sum($values) / count($values) , $pricision);
				}
				
				$words_props_from_api[$word]['avg_price'] = round( $avg_price_nu / $total_competition );
				$words_props_from_api[$word]['ctr'] = $words_props_from_api[$word]['pv'] == 0 ? 0 
						: 100 * $words_props_from_api[$word]['click'] / $words_props_from_api[$word]['pv'];
				$words_props_from_api[$word] = array_intersect_key( $words_props_from_api[$word] ,$key_fields );
			}
			
			//处理垃圾词,即其他词有数据但是这个词语没有数据
			$junky_words = array_diff( $need_api_words[$i] , $words_with_data );
			$this->_junky_words = array_unique( array_merge( $this->_junky_words , $junky_words ) );
			
			//得到子类中可能存在的保存数据的办法
			$this->checkSaveData('word_basic_props' , $dataList);
		}
		
		return	$words_props_from_api;
	}
	
	
	protected function getSerialProps($words ,$fields , $startDate, $endDate){
		//跳过参数验证，所有参数验证必须在对外公开的类中进行，所有api方法不对外公开
		//分成小块查询，当需要查询的词语比较多的时候
		$need_api_words = array_chunk($words, self::MAX_WORDS_BASIC_PROPS_PER_REQUEST);
		//调用子类方法
		$apiMdl = $this->_getApiMdl();
		$words_series_from_api = array();
		$startTime = strtotime($startDate);
		$endTime = strtotime($endDate);
		
		for( $i=0; $i < count($need_api_words); $i++ ){
			$words_with_data = array();
			//一次性查3个月的表现,当出现api错误的时候跳过此chunk，本组词全部为NULL
			$results = $apiMdl->getWordProps( $need_api_words[$i] , '3MONTH');
		
			//save to db
			$dataList = array();
			$key_fields = array_flip( $fields );
			$key_fields['date'] = null;
			foreach( $results as $word_in_records){
				$word =  $word_in_records->word;
				$words_with_data[] = $word;
				$words_series_from_api[ $word ] = array();
				foreach( $word_in_records->in_record_base_list->i_n_record_base as $row ){
					$dataList[] = $row;
					/*
					$row = (array) $row;
					$row['word'] = $word;
					$dataList[] = $row;
					//满足时间条件的记录保留下来用来显示
					if( strtotime($row['date']) <= $endTime && strtotime($row['date']) >= $startTime ){
						$words_series_from_api[$word][] = array_intersect_key( $row ,$key_fields );
					}*/
				}
			}

			//处理垃圾词,即其他词有数据但是这个词语没有数据
			$junky_words = array_diff( $need_api_words[$i] , $words_with_data );
			$this->_junky_words = array_unique( array_merge( $this->_junky_words , $junky_words ) );
			
			//检查是否需要实时保存数据
            $this->checkSaveData('word_basic_props' , $dataList);
		}
		
		return $words_series_from_api;
				
	}
	
	/**
	 * 查询某个或者多个关键词的最新统计属性分布，基于效率建议一次只能获取某个指标的数据，多增加一个指标需要的api请求次数增加一倍
	 *
	 * @param string $word 关键词
	 * @param string $field AREA|HPRICE|SOURCE 
	 * @param bool $useApi 在数据库未查到的情况下, 是否调用API查询.
	 * @return JSON String 形如: {115,1,240,1,110,3,35,165,195,1,280,1,190,1,155,1,150,9}
	 */
	protected function getWordAnalysis($words, $field) {
		$need_api_words = array_chunk($words, self::MAX_WORDS_ANALYSIS_PROPS_PER_REQUEST);
		$apiMdl = $this->_getApiMdl();
		$dataList = array();
		$field_api_map = array(
					'HPRICE' => 'word_hp_price',
					'AREA' => 'word_area_per',
					'SOURCE' => 'word_source_per'
				);
		$words_analysis = array_fill_keys( $words, array() );
		for( $i=0; $i < count($need_api_words); $i++ ){
			$words_with_data = array();
		    $words = $need_api_words[$i];
			$retry = 0;
			try{
			    $results = $apiMdl->getWordAnalysis($words, $field);
			}catch(Exception $e){
			    if($retry > 3){
			        throw $e;
			    }
    			$sliceSize = ceil( count($words) / 2);
    			$part1 = array_slice($words, 0, $sliceSize);
    			$part2 = array_slice($words, $sliceSize);

			    $results = array_merge(
			                    $apiMdl->getWordAnalysis($part1, $field),
			                    $apiMdl->getWordAnalysis($part2, $field)
			                );
			                    
			    $retry++;
			}
			
			if( $results === null ) continue;
			
			foreach( $results as $row){
				$words_analysis[$row->word] = array(
							strtolower($field) => $row->$field_api_map[ strtoupper($field) ] 
						);
				$words_with_data[] = $row->word;
				$dataList[$row->word] = array(
							'word' => $row->word,
							'date' => date('Y-m-d' , strtotime('-1 day')),
							strtolower($field) => $row->$field_api_map[ strtoupper($field) ] 
						);
			}
			
			//处理垃圾词,即其他词有数据但是这个词语没有数据
			$junky_words = array_diff( $need_api_words[$i] , $words_with_data );
			$this->_junky_words = array_unique( array_merge( $this->_junky_words , $junky_words ) );
			
		}
		
		//检查是否需要实时保存数据
		$this->checkSaveData('word_analysis_props' , $dataList , array('source' => 'api'));
	
		return $words_analysis;
	}
	
	/**
	 * @deprecated 升级到新版本api
	 * @TODO 优化一下请求次数，最差的情况每个词都需要一个api去查询
	 * 获取一个或者多个词语的关键词列表
	 * @param $words ，需要查询的关键词相关词列表，可以是单个，多个以逗号或者array(string)的形式传入， 最长100个
	 * @param $mergeRes ， 当为true时候只请求一次，返回结果不区分关键词，当需要区分每个关键词对应的相关词时，设为false
	 * @return 返回的关键词相关词列表，
	 * 			$mergeRes = false时候返回结果如下
	 * 			array(
	 * 				'word1' => 'w1,w2,w3',
	 * 				...	
	 * 			);
	 * 			$mergeRes = true时候返回结果如下
	 * 			array( w1 ,w2 ,w3, w4)
	 * 			
	 */
	protected function getWordsRelated($words , $mergeRes=true){
		if(!is_array($words)) $words = explode(',' , $words);
		
		//大词组在WordModel::getWordsRelated做了分隔再传入
		if( count($words) > 100 ){
			throw new NewzanException("目前api查询关键词的相关词限制在100个以内" , RetStatus::INNER_ERR);	
		}
		
		if(!$words) return array();
		$len = count($words);
		$apiMdl = $this->_getApiMdl();
		$relatedWords = $apiMdl->getRelatedWords($words, self::MAX_API_RELATED_WORDS); // 查API
		$relatedWords  = $relatedWords ? $relatedWords : array();
		//如果是综合查询，则不需要分割返回结果，直接返回就可以了
		if(!$mergeRes){
    		// 得到的结果如果不等于10 * 词数量，说明每个词每个词得到的10个没有查到
    		//  需要划分成更小的单位查询，只到只剩一个词语
    		$enough = count($relatedWords) == self::MAX_API_RELATED_WORDS * count($words);
    		if( $enough || count($words) == 1) {
        		//把词语按10个一个分开，对应到每个词
        		$chunked_relatedWords = array_chunk($relatedWords, self::MAX_API_RELATED_WORDS);
        		$relatedWords = array_combine($words, $chunked_relatedWords);
                $relatedWords = $relatedWords ? $relatedWords : array();
    		}elseif( count($words) > 1 ){
    			$sliceSize = ceil($len / 2);
    			$part1 = array_slice($words, 0, $sliceSize);
    			$part2 = array_slice($words, $sliceSize);
    			//递归
    			$relatedWords = array_replace_recursive(
                    			$this->getWordsRelated($part1 , false),
                    			$this->getWordsRelated($part2 , false) 
                    	  );
    		}
    	    //检查是否需要实时保存数据
    		$this->checkSaveData('word_related' , $relatedWords , array('source' => 'api') );
    		//$this->_getWordRelatedMdl()->saveDataList()
		}
		return $relatedWords;
	}
	
	/**
	 * 
	 * 获取一些词语的淘宝搜索相关词
	 * @param mixed $words , words列表数组array( w1, w2, w3);
	 */
	public function getSearchRelated($words){
	    $apiMdl = $this->_getApiMdl();
	    $dataList = array();
		foreach($words as $word) {
            //两次失败后本词语跳过		    
			$relatedWords = $apiMdl->getSearchRealtedWords($word, 2);
			if($relatedWords){
			    $dataList[$word] = $relatedWords;
			}
		}

		//检查是否需要实时保存数据
        $this->checkSaveData('word_related' , $dataList , array('source' => 'search') );
        return $dataList;
	}
	
	/**
	 * 
	 * 获取类目的热门词
	 * @param unknown_type $cate_ids
	 * @param unknown_type $num
	 * @param unknown_type $merge
	 * @param unknown_type $useApi
	 */
	protected function getCatHotWords($cate_ids , $num = 100 ,$merge = true ){
	    if( empty($cate_ids) ){
	        return array();
	    }
	    
        $apiMdl = $this->_getApiMdl();
        $len = count($cate_ids);
        $hot_words = $apiMdl->getCateHotWords($cate_ids , $num);
        //filter some terrible words
        if(!$merge){
    		$enough = count($hot_words) == $num * $len;
    		//如果数量是对的或者只剩下一个词语了或者不需要分开类目显示结果则可以返回了
    		if( $enough || count($words) == 1 || $merge) {
        		$chunked_hot_words = array_chunk($hot_words, $num);
        		$hot_words = array_combine($cate_ids, $chunked_hot_words);
                $hot_words = $hot_words ? $hot_words : array();
    		}elseif( count($words) > 1 ){
    			$sliceSize = ceil($len / 2);
    			$part1 = array_slice($cate_ids, 0, $sliceSize);
    			$part2 = array_slice($cate_ids, $sliceSize);
    			//递归
    			$hot_words = array_replace_recursive(
                    			$this->getCatHotWords($part1 , $num , $merge),
                    			$this->getCatHotWords($part2 , $num , $merge) 
                    	  );
    		}
    	    //不merge的时候才能区分什么检查是否需要实时保存数据
    	    $this->checkSaveData( 'word_hot' , $hot_words );
        }
        return $hot_words;
	}
	
	
	/**
	 * 通过API获取某个词在类目下的基础统计属性，包括pv, ctr, avg_price ,competition. 并且保存到数据库中.
	 *
	 * @param array $words_withId array( word => word_id , word => word_id) $words ， 以word本身为下标，word_id为值的数组，<strong>必须要这样传</strong>
	 * @param int $cateId.
	 * @return 2D array , example，以每个词为下标的2d数组，每个数组包含每个词对应的统计属性的数组。
	 *  							类目下无统计值的关键词所有统计属性为0
	 */
	protected function getWordCatsBasicAvgProps($words, $cateId){
		//判断大小，分块处理，100为一个块
		$dataList = array();
		$word_cats = $this->getCateWordStatsRecursive($words , $cateId );
		//object to array
		$word_cats = object_to_array($word_cats);
		$words_result = array();
		$found_date = '';
		foreach($word_cats as $word){
			$words_result[ $word['word'] ] = $word;
			$words_result[ $word['word'] ]['click'] = round( $word['pv'] * $word['ctr'] / 100 );
			$found_date = $word['date'];
		}
		//合并结果，对于无返回值的全部赋值0
		$found_date = $found_date ? $found_date : date('Y-m-d');
		foreach( $words as $word){
			if( !isset($words_result[$word]) ){
				$words_result[$word] = array(
						'avg_price' => 0,
						'category_id' => $cateId,
						'click' => 0,
						'competition' => 0,
						'ctr' => 0,
						'date' => $found_date, // date('Y-m-d' , time("-1 day")), keep it the same as those who have values!!!
						'pv' => 0,
						'word' =>  $word
				);
			}
		}
		//$this->logDebug('array_values( $words_result ): ');
		//$this->logDebug(json_encode(array_values( $words_result )));
		$this->checkSaveData( 'word_cats_props', $words_result);
	
		return $words_result;
	}
	
	protected function getCategoryAvgBase( $cates_id , $fields , $startTime = null, $endTime = null){
		if( is_array( $cates_id ) ){
			$cates_id = implode(',' , $cates_id );
		}
		$apiMdl = $this->_getApiMdl();
		$res = $apiMdl->request( 'SimbaInsightCatsbaseGetRequest' , array(
				'Time' => 'MONTH',
				'CategoryIds' => $cates_id,
				'Filter' => 'PV,CLICK,AVGCPC,COMPETITION,CTR'
		));
		$dataList = array();
		$cate_data = array();
		$res = object_to_array( $res );
		if( isset( $res['in_category_bases']['i_n_category_base'] )){
			foreach( $res['in_category_bases']['i_n_category_base'] as $cate_base){
				$category_id = $cate_base['category_id'];
				$cate_data[ $category_id ] = array();
				$total_competition = 0;
				$avg_price_nu = 0;
				foreach( $cate_base['in_record_base_list']['i_n_record_base']  as $row){
					$row['category_id'] = $category_id;
					$dataList[] = $row;
					if( strtotime($row['date']) <= $endTime && strtotime($row['date']) >= $startTime ){
						$cate_data[ $category_id ] = array_merge_recursive($row , $cate_data[$category_id]);
						$total_competition += intval($row['competition']);
						$avg_price_nu += intval($row['avg_price']) * intval($row['competition']);
					}
				}
				
				//日期属性去掉，因为是求平均值
				unset($cate_data[$category_id]['date']);
				foreach( $cate_data[$category_id]  as  $wk => $values) {
						$cate_data[$category_id][$wk] = round( array_sum($values) / count($values) , 0);
				}
				$cate_data[$category_id]['ctr'] = round(100 * $cate_data[$category_id]['click'] / $cate_data[$category_id]['pv'] , 2);
					
				$cate_data[$category_id]['avg_price'] = round( $avg_price_nu / $total_competition );
			}
		}

		//$this->checkSaveData( 'category_props', $dataList);
		return $cate_data;
	}
	
	
	/**
	* 递归（调用API）获取词在类目中的基础属性.
	*
	* @param array(string) $words 最大数目不超过100.
	* @param int $cateId
	* @param ApiModel $apiMdl
	* @return array(object) The words that have no record in the given category will not be shown in the result.
	* 				如果结果为空, 返回空数组array().
	*/
	public function getCateWordStatsRecursive($words, $cateId) {
		if(empty($words))
			return array();
	    $apiMdl = $this->_getApiMdl();
		$result = array();
		try {
			$result = $apiMdl->getCateWordsStats($words, $cateId);
		} catch (Exception $e) {
			if(getTopErrorCode($e->getMessage()) == 27 || count($words) <= 50) { // session expired or canot split anymore
				throw $e;
			} else {
				$len = count($words);
				$sliceSize = ceil($len / 2);
				$part1 = array_slice($words, 0, $sliceSize);
				$part2 = array_slice($words, $sliceSize);
				$result = array_merge(
						$this->getCateWordStatsRecursive($part1, $cateId),
						$this->getCateWordStatsRecursive($part2, $cateId) );
			}
		}
		return $result;
	}
	
	

	/**
	 * @return ApiModel
	 */
	protected function _getApiMdl(){
		if($this->_apiMdl == null) {
			$this->_apiMdl = new ApiModel();
		}
		return $this->_apiMdl;
	}
	
	
}
